{
  "nbformat": 4,
  "nbformat_minor": 0,
  "metadata": {
    "colab": {
      "provenance": [],
      "authorship_tag": "ABX9TyMUv5QS+QIGtfLmdfd5O4sU",
      "include_colab_link": true
    },
    "kernelspec": {
      "name": "python3",
      "display_name": "Python 3"
    },
    "language_info": {
      "name": "python"
    }
  },
  "cells": [
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "view-in-github",
        "colab_type": "text"
      },
      "source": [
        "<a href=\"https://colab.research.google.com/github/DanielWarfield1/MLWritingAndResearch/blob/main/LangGraph.ipynb\" target=\"_parent\"><img src=\"https://colab.research.google.com/assets/colab-badge.svg\" alt=\"Open In Colab\"/></a>"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 1,
      "metadata": {
        "id": "TAMvnYYuPyYc",
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "outputId": "aa86e198-e07a-40de-e69a-d8eb2964b083"
      },
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "Requirement already satisfied: langgraph in /usr/local/lib/python3.10/dist-packages (0.2.16)\n",
            "Requirement already satisfied: langchain-openai in /usr/local/lib/python3.10/dist-packages (0.1.23)\n",
            "Requirement already satisfied: langchain-core<0.3,>=0.2.27 in /usr/local/lib/python3.10/dist-packages (from langgraph) (0.2.37)\n",
            "Requirement already satisfied: langgraph-checkpoint<2.0.0,>=1.0.2 in /usr/local/lib/python3.10/dist-packages (from langgraph) (1.0.8)\n",
            "Requirement already satisfied: openai<2.0.0,>=1.40.0 in /usr/local/lib/python3.10/dist-packages (from langchain-openai) (1.43.0)\n",
            "Requirement already satisfied: tiktoken<1,>=0.7 in /usr/local/lib/python3.10/dist-packages (from langchain-openai) (0.7.0)\n",
            "Requirement already satisfied: PyYAML>=5.3 in /usr/local/lib/python3.10/dist-packages (from langchain-core<0.3,>=0.2.27->langgraph) (6.0.2)\n",
            "Requirement already satisfied: jsonpatch<2.0,>=1.33 in /usr/local/lib/python3.10/dist-packages (from langchain-core<0.3,>=0.2.27->langgraph) (1.33)\n",
            "Requirement already satisfied: langsmith<0.2.0,>=0.1.75 in /usr/local/lib/python3.10/dist-packages (from langchain-core<0.3,>=0.2.27->langgraph) (0.1.108)\n",
            "Requirement already satisfied: packaging<25,>=23.2 in /usr/local/lib/python3.10/dist-packages (from langchain-core<0.3,>=0.2.27->langgraph) (24.1)\n",
            "Requirement already satisfied: pydantic<3,>=1 in /usr/local/lib/python3.10/dist-packages (from langchain-core<0.3,>=0.2.27->langgraph) (2.8.2)\n",
            "Requirement already satisfied: tenacity!=8.4.0,<9.0.0,>=8.1.0 in /usr/local/lib/python3.10/dist-packages (from langchain-core<0.3,>=0.2.27->langgraph) (8.5.0)\n",
            "Requirement already satisfied: typing-extensions>=4.7 in /usr/local/lib/python3.10/dist-packages (from langchain-core<0.3,>=0.2.27->langgraph) (4.12.2)\n",
            "Requirement already satisfied: anyio<5,>=3.5.0 in /usr/local/lib/python3.10/dist-packages (from openai<2.0.0,>=1.40.0->langchain-openai) (3.7.1)\n",
            "Requirement already satisfied: distro<2,>=1.7.0 in /usr/lib/python3/dist-packages (from openai<2.0.0,>=1.40.0->langchain-openai) (1.7.0)\n",
            "Requirement already satisfied: httpx<1,>=0.23.0 in /usr/local/lib/python3.10/dist-packages (from openai<2.0.0,>=1.40.0->langchain-openai) (0.27.2)\n",
            "Requirement already satisfied: jiter<1,>=0.4.0 in /usr/local/lib/python3.10/dist-packages (from openai<2.0.0,>=1.40.0->langchain-openai) (0.5.0)\n",
            "Requirement already satisfied: sniffio in /usr/local/lib/python3.10/dist-packages (from openai<2.0.0,>=1.40.0->langchain-openai) (1.3.1)\n",
            "Requirement already satisfied: tqdm>4 in /usr/local/lib/python3.10/dist-packages (from openai<2.0.0,>=1.40.0->langchain-openai) (4.66.5)\n",
            "Requirement already satisfied: regex>=2022.1.18 in /usr/local/lib/python3.10/dist-packages (from tiktoken<1,>=0.7->langchain-openai) (2024.5.15)\n",
            "Requirement already satisfied: requests>=2.26.0 in /usr/local/lib/python3.10/dist-packages (from tiktoken<1,>=0.7->langchain-openai) (2.32.3)\n",
            "Requirement already satisfied: idna>=2.8 in /usr/local/lib/python3.10/dist-packages (from anyio<5,>=3.5.0->openai<2.0.0,>=1.40.0->langchain-openai) (3.8)\n",
            "Requirement already satisfied: exceptiongroup in /usr/local/lib/python3.10/dist-packages (from anyio<5,>=3.5.0->openai<2.0.0,>=1.40.0->langchain-openai) (1.2.2)\n",
            "Requirement already satisfied: certifi in /usr/local/lib/python3.10/dist-packages (from httpx<1,>=0.23.0->openai<2.0.0,>=1.40.0->langchain-openai) (2024.7.4)\n",
            "Requirement already satisfied: httpcore==1.* in /usr/local/lib/python3.10/dist-packages (from httpx<1,>=0.23.0->openai<2.0.0,>=1.40.0->langchain-openai) (1.0.5)\n",
            "Requirement already satisfied: h11<0.15,>=0.13 in /usr/local/lib/python3.10/dist-packages (from httpcore==1.*->httpx<1,>=0.23.0->openai<2.0.0,>=1.40.0->langchain-openai) (0.14.0)\n",
            "Requirement already satisfied: jsonpointer>=1.9 in /usr/local/lib/python3.10/dist-packages (from jsonpatch<2.0,>=1.33->langchain-core<0.3,>=0.2.27->langgraph) (3.0.0)\n",
            "Requirement already satisfied: orjson<4.0.0,>=3.9.14 in /usr/local/lib/python3.10/dist-packages (from langsmith<0.2.0,>=0.1.75->langchain-core<0.3,>=0.2.27->langgraph) (3.10.7)\n",
            "Requirement already satisfied: annotated-types>=0.4.0 in /usr/local/lib/python3.10/dist-packages (from pydantic<3,>=1->langchain-core<0.3,>=0.2.27->langgraph) (0.7.0)\n",
            "Requirement already satisfied: pydantic-core==2.20.1 in /usr/local/lib/python3.10/dist-packages (from pydantic<3,>=1->langchain-core<0.3,>=0.2.27->langgraph) (2.20.1)\n",
            "Requirement already satisfied: charset-normalizer<4,>=2 in /usr/local/lib/python3.10/dist-packages (from requests>=2.26.0->tiktoken<1,>=0.7->langchain-openai) (3.3.2)\n",
            "Requirement already satisfied: urllib3<3,>=1.21.1 in /usr/local/lib/python3.10/dist-packages (from requests>=2.26.0->tiktoken<1,>=0.7->langchain-openai) (2.0.7)\n"
          ]
        }
      ],
      "source": [
        "# %pip install --quiet -U langgraph langchain-community langchain-openai\n",
        "!pip install langgraph langchain-openai"
      ]
    },
    {
      "cell_type": "code",
      "source": [
        "import os\n",
        "from google.colab import userdata\n",
        "\n",
        "os.environ['OPENAI_API_KEY'] = userdata.get('OpenAIAPIKey')"
      ],
      "metadata": {
        "id": "oO_H5d_dP3gI"
      },
      "execution_count": 2,
      "outputs": []
    },
    {
      "cell_type": "markdown",
      "source": [
        "# Making a simple graph that runs"
      ],
      "metadata": {
        "id": "GKzuWfQEWvBH"
      }
    },
    {
      "cell_type": "code",
      "source": [
        "from typing import TypedDict\n",
        "from langgraph.graph import StateGraph, START, END\n",
        "\n",
        "# Defining state\n",
        "class GraphState(TypedDict):\n",
        "    incrementor: int\n",
        "\n",
        "workflow = StateGraph(GraphState)\n",
        "\n",
        "# I still have no idea why I need to return the state in this example, and\n",
        "# I don't in the previous example. But, to me this is more explicit so whatever.\n",
        "def handle_hello_world(state):\n",
        "    print('Hello World')\n",
        "    state['incrementor'] += 1  # Correctly update the state\n",
        "    return state  # Return the modified state\n",
        "\n",
        "# Add the node to the graph\n",
        "workflow.add_node(\"hello_world\", handle_hello_world)\n",
        "\n",
        "# Set entry point and edge\n",
        "workflow.set_entry_point(\"hello_world\")\n",
        "workflow.add_edge('hello_world', END)\n",
        "\n",
        "# Compile and run the workflow\n",
        "app = workflow.compile()\n",
        "inputs = {\"incrementor\": 0}  # Provide the initial state\n",
        "result = app.invoke(inputs)\n",
        "print(result)"
      ],
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "Q21b_f_zc9ZU",
        "outputId": "89d47e67-dd00-4263-9f06-2fdd6627393a"
      },
      "execution_count": 3,
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "Hello World\n",
            "{'incrementor': 1}\n"
          ]
        }
      ]
    },
    {
      "cell_type": "markdown",
      "source": [
        "# Making a simple graph that introduces itself, asks for a name, then updates the state"
      ],
      "metadata": {
        "id": "zK1AOqp-e3Hw"
      }
    },
    {
      "cell_type": "code",
      "source": [
        "from typing import TypedDict\n",
        "from langgraph.graph import StateGraph, START, END\n",
        "\n",
        "# Defining state\n",
        "class GraphState(TypedDict):\n",
        "    name: str\n",
        "    incrementor: int\n",
        "\n",
        "workflow = StateGraph(GraphState)\n",
        "\n",
        "# I still have no idea why I need to return the state in this example, and\n",
        "# I don't in the previous example. But, to me this is more explicit so whatever.\n",
        "def handle_intro(state):\n",
        "    state['incrementor'] += 1\n",
        "    print('Hello!')\n",
        "    return state  # Does not modify state\n",
        "\n",
        "def handle_name_request(state):\n",
        "    state['incrementor'] += 1\n",
        "    state['name'] = input('What is your name? ')\n",
        "    return state\n",
        "\n",
        "# Adding Nodes to Graph\n",
        "workflow.add_node(\"intro\", handle_intro)\n",
        "workflow.add_node(\"name_request\", handle_name_request)\n",
        "\n",
        "# Set entry point and edge\n",
        "workflow.set_entry_point(\"intro\")\n",
        "workflow.add_edge('intro', 'name_request')\n",
        "workflow.add_edge('name_request', END)\n",
        "\n",
        "# Compile and run the workflow\n",
        "app = workflow.compile()\n",
        "inputs = {\"incrementor\": 0}  # Provide the initial state\n",
        "result = app.invoke(inputs)\n",
        "print('output state:')\n",
        "print(result)"
      ],
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "UYLb6WAtdzH5",
        "outputId": "df26010f-90ee-4647-bfd7-195a53565638"
      },
      "execution_count": 4,
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "Hello!\n",
            "What is your name? dan\n",
            "output state:\n",
            "{'name': 'dan', 'incrementor': 2}\n"
          ]
        }
      ]
    },
    {
      "cell_type": "markdown",
      "source": [
        "# Parsing first name and last name"
      ],
      "metadata": {
        "id": "be_GjJyrhZox"
      }
    },
    {
      "cell_type": "code",
      "source": [
        "from typing import TypedDict\n",
        "from langgraph.graph import StateGraph, START, END\n",
        "from langchain_openai import ChatOpenAI\n",
        "from langchain_core.prompts import ChatPromptTemplate\n",
        "\n",
        "# Defining state\n",
        "class GraphState(TypedDict):\n",
        "    first_name: str\n",
        "    last_name: str\n",
        "    incrementor: int\n",
        "\n",
        "workflow = StateGraph(GraphState)\n",
        "\n",
        "def handle_intro(state):\n",
        "    state['incrementor'] += 1\n",
        "    print('Hello!')\n",
        "    return state  # Does not modify state\n",
        "\n",
        "class Name(TypedDict):\n",
        "    first_name: str\n",
        "    last_name: str\n",
        "\n",
        "name_parse_prompt = ChatPromptTemplate.from_messages(\n",
        "    [\n",
        "        (\n",
        "            \"system\",\n",
        "            \"\"\"Parse the first and last name from the users message\"\"\",\n",
        "        ),\n",
        "        (\"placeholder\", \"{messages}\"),\n",
        "    ]\n",
        ")\n",
        "\n",
        "name_parser = name_parse_prompt | ChatOpenAI(\n",
        "    model=\"gpt-4o\", temperature=0\n",
        ").with_structured_output(Name)\n",
        "\n",
        "def handle_name_request(state):\n",
        "    state['incrementor'] += 1\n",
        "    user_reponse = input('What is your first and last name? ')\n",
        "    name = name_parser.invoke({\"messages\": [(\"ai\", \"What is your first and last name?\"),(\"user\", user_reponse)]})\n",
        "    state['first_name'] = name['first_name']\n",
        "    state['last_name'] = name['last_name']\n",
        "    return state\n",
        "\n",
        "# Adding Nodes to Graph\n",
        "workflow.add_node(\"intro\", handle_intro)\n",
        "workflow.add_node(\"name_request\", handle_name_request)\n",
        "\n",
        "# Set entry point and edge\n",
        "workflow.set_entry_point(\"intro\")\n",
        "workflow.add_edge('intro', 'name_request')\n",
        "workflow.add_edge('name_request', END)\n",
        "\n",
        "# Compile and run the workflow\n",
        "app = workflow.compile()\n",
        "inputs = {\"incrementor\": 0}  # Provide the initial state\n",
        "result = app.invoke(inputs)\n",
        "print('output state:')\n",
        "print(result)"
      ],
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "z-P39JCTfrRL",
        "outputId": "e542d551-aca3-4547-b819-3ce436002aa6"
      },
      "execution_count": 5,
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "Hello!\n",
            "What is your first and last name? dan w\n",
            "output state:\n",
            "{'first_name': 'dan', 'last_name': 'w', 'incrementor': 2}\n"
          ]
        }
      ]
    },
    {
      "cell_type": "markdown",
      "source": [
        "# Checking for full first and last name\n",
        "if the name is not full, re-ask"
      ],
      "metadata": {
        "id": "hPHWJ58rlnnL"
      }
    },
    {
      "cell_type": "code",
      "source": [
        "from typing import TypedDict\n",
        "from langgraph.graph import StateGraph, START, END\n",
        "from langchain_openai import ChatOpenAI\n",
        "from langchain_core.prompts import ChatPromptTemplate\n",
        "\n",
        "# Defining state\n",
        "class GraphState(TypedDict):\n",
        "    first_name: str\n",
        "    last_name: str\n",
        "    incrementor: int\n",
        "\n",
        "workflow = StateGraph(GraphState)\n",
        "\n",
        "# ===========================\n",
        "def handle_intro(state):\n",
        "    state['incrementor'] += 1\n",
        "    print('Hello!')\n",
        "    return state  # Does not modify state\n",
        "\n",
        "workflow.add_node(\"intro\", handle_intro)\n",
        "# ===========================\n",
        "class Name(TypedDict):\n",
        "    first_name: str\n",
        "    last_name: str\n",
        "\n",
        "name_parse_prompt = ChatPromptTemplate.from_messages(\n",
        "    [\n",
        "        (\n",
        "            \"system\",\n",
        "            \"\"\"Parse the first and last name from the users message\"\"\",\n",
        "        ),\n",
        "        (\"placeholder\", \"{messages}\"),\n",
        "    ]\n",
        ")\n",
        "\n",
        "name_parser = name_parse_prompt | ChatOpenAI(\n",
        "    model=\"gpt-4o\", temperature=0\n",
        ").with_structured_output(Name)\n",
        "\n",
        "def handle_name_request(state):\n",
        "    state['incrementor'] += 1\n",
        "    user_reponse = input('What is your full first and last name? ')\n",
        "    name = name_parser.invoke({\"messages\": [(\"ai\", \"What is your full first and last name?\"),(\"user\", user_reponse)]})\n",
        "    state['first_name'] = name['first_name']\n",
        "    state['last_name'] = name['last_name']\n",
        "    return state\n",
        "\n",
        "workflow.add_node(\"name_request\", handle_name_request)\n",
        "# ===========================\n",
        "class IsFullName(TypedDict):\n",
        "    is_full_name: bool\n",
        "\n",
        "name_check_prompt = ChatPromptTemplate.from_messages(\n",
        "    [\n",
        "        (\n",
        "            \"system\",\n",
        "            \"\"\"Is the name a full and complete first and last name?\"\"\",\n",
        "        ),\n",
        "        (\"placeholder\", \"{messages}\"),\n",
        "    ]\n",
        ")\n",
        "\n",
        "name_checker = name_check_prompt | ChatOpenAI(\n",
        "    model=\"gpt-4o\", temperature=0\n",
        ").with_structured_output(IsFullName)\n",
        "\n",
        "def check_full_name(state):\n",
        "    state['incrementor'] += 1\n",
        "    name_check = name_checker.invoke({\"messages\": [(\"user\", f\"first name: \\\"{state['first_name']}\\\", last name: \\\"{state['last_name']}\\\"\")]})\n",
        "    if name_check['is_full_name']:\n",
        "        return '__end__'\n",
        "    else:\n",
        "        print('The name provided was not complete')\n",
        "        return 'name_request'\n",
        "# ===========================\n",
        "\n",
        "# Set entry point and edge\n",
        "workflow.set_entry_point(\"intro\")\n",
        "workflow.add_edge('intro', 'name_request')\n",
        "workflow.add_conditional_edges(\n",
        "    'name_request',\n",
        "    check_full_name,\n",
        "    [\"name_request\", \"__end__\"]\n",
        ")\n",
        "\n",
        "# Compile and run the workflow\n",
        "app = workflow.compile()\n",
        "inputs = {\"incrementor\": 0}  # Provide the initial state\n",
        "result = app.invoke(inputs)\n",
        "print('output state:')\n",
        "print(result)"
      ],
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "YSfsxNiNkoY_",
        "outputId": "177e6145-ce73-4f62-d4e7-12ac5ce1e776"
      },
      "execution_count": 6,
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "Hello!\n",
            "What is your full first and last name? dan w\n",
            "The name provided was not complete\n",
            "What is your full first and last name? daniel warfield\n",
            "output state:\n",
            "{'first_name': 'daniel', 'last_name': 'warfield', 'incrementor': 3}\n"
          ]
        }
      ]
    },
    {
      "cell_type": "code",
      "source": [
        "from IPython.display import Image, display\n",
        "display(Image(app.get_graph(xray=True).draw_mermaid_png()))"
      ],
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/",
          "height": 320
        },
        "id": "kEY-bjFRukei",
        "outputId": "e889a0b8-d4ff-4018-a819-685229b98c50"
      },
      "execution_count": 7,
      "outputs": [
        {
          "output_type": "display_data",
          "data": {
            "image/jpeg": "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\n",
            "text/plain": [
              "<IPython.core.display.Image object>"
            ]
          },
          "metadata": {}
        }
      ]
    },
    {
      "cell_type": "markdown",
      "source": [
        "# Making it more conversational\n",
        "The requests are hard coded. In this modification I'll keep track of all the dialogue between the system, and all the decisions, and use that to inform the language model of how to communicate with the user."
      ],
      "metadata": {
        "id": "-CScjKlo7adP"
      }
    },
    {
      "cell_type": "code",
      "source": [
        "from typing import TypedDict\n",
        "from langgraph.graph import StateGraph, START, END\n",
        "from langchain_openai import ChatOpenAI\n",
        "from langchain_core.prompts import ChatPromptTemplate\n",
        "\n",
        "# Defining state\n",
        "class GraphState(TypedDict):\n",
        "    first_name: str\n",
        "    last_name: str\n",
        "    incrementor: int\n",
        "    conversation: list[tuple[str]]\n",
        "\n",
        "workflow = StateGraph(GraphState)\n",
        "\n",
        "# ===========================\n",
        "class Name(TypedDict):\n",
        "    first_name: str\n",
        "    last_name: str\n",
        "\n",
        "name_parse_prompt = ChatPromptTemplate.from_messages(\n",
        "    [\n",
        "        (\n",
        "            \"system\",\n",
        "            \"\"\"Parse the first and last name from the users message\"\"\",\n",
        "        ),\n",
        "        (\"placeholder\", \"{messages}\"),\n",
        "    ]\n",
        ")\n",
        "\n",
        "name_parser = name_parse_prompt | ChatOpenAI(\n",
        "    model=\"gpt-4o\", temperature=0\n",
        ").with_structured_output(Name)\n",
        "\n",
        "def handle_name_request(state):\n",
        "    state['incrementor'] += 1\n",
        "\n",
        "    #if the conversation has just started, append a system prompt\n",
        "    if state['conversation'] is None:\n",
        "        system_prompt = '''\n",
        "        You are an AI agent tasked with doing lead qualification for a real-estate\n",
        "        company. Your present objective is to introduce yourself as \"Rachael\", say\n",
        "        you're excited to help them get set up with a new home. After you introduce yourself,\n",
        "        ask them for their name in full so you can get started.\n",
        "        '''\n",
        "        state['conversation'] = [('system', system_prompt)]\n",
        "\n",
        "\n",
        "    chat = ChatOpenAI(model=\"gpt-4o\", temperature=0)\n",
        "    ai_request_to_user = chat.invoke(state['conversation']).content\n",
        "\n",
        "    user_reponse = input(ai_request_to_user +'\\n')\n",
        "    state['conversation'].append(('user', user_reponse))\n",
        "    name = name_parser.invoke({\"messages\": state['conversation']})\n",
        "    state['first_name'] = name.get('first_name', '')\n",
        "    state['last_name'] = name.get('last_name', '')\n",
        "    return state\n",
        "\n",
        "workflow.add_node(\"name_request\", handle_name_request)\n",
        "# ===========================\n",
        "class IsAcceptable(TypedDict):\n",
        "    is_acceptable: bool\n",
        "\n",
        "name_check_prompt = ChatPromptTemplate.from_messages(\n",
        "    [\n",
        "        (\n",
        "            \"system\",\n",
        "            \"\"\"Is the name a full and complete first and last name? The name should either\n",
        "be an obvious full legal name, or the user should have confirmed that it is their full name.\n",
        "If either are true, then the name is considered acceptable.\"\"\",\n",
        "        ),\n",
        "        (\"placeholder\", \"{messages}\"),\n",
        "    ]\n",
        ")\n",
        "\n",
        "name_checker = name_check_prompt | ChatOpenAI(\n",
        "    model=\"gpt-4o\", temperature=0\n",
        ").with_structured_output(IsAcceptable)\n",
        "\n",
        "def check_full_name(state):\n",
        "    state['incrementor'] += 1\n",
        "\n",
        "    name_check = name_checker.invoke({\"messages\": state['conversation']+[(\"user\", f\"first name: \\\"{state['first_name']}\\\", last name: \\\"{state['last_name']}\\\"\")]})\n",
        "    if name_check['is_acceptable']:\n",
        "        return '__end__'\n",
        "    else:\n",
        "        system_prompt = '''\n",
        "        The provided name appears to be incomplete. Please notify the user\n",
        "that the name does not appear to be complete and request that they either\n",
        "provide their full name or confirm that that is indeed their full name. You don't need to re-introduce yourself anymore.\n",
        "        '''\n",
        "        state['conversation'].append(('system', system_prompt))\n",
        "        return 'name_request'\n",
        "# ===========================\n",
        "\n",
        "# Set entry point and edge\n",
        "workflow.set_entry_point('name_request')\n",
        "workflow.add_conditional_edges(\n",
        "    'name_request',\n",
        "    check_full_name,\n",
        "    [\"name_request\", \"__end__\"]\n",
        ")\n",
        "\n",
        "# Compile and run the workflow\n",
        "app = workflow.compile()\n",
        "inputs = {\"incrementor\": 0, \"conversation\":None}  # Provide the initial state\n",
        "result = app.invoke(inputs)\n",
        "print('output state:')\n",
        "print(result)"
      ],
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "2MD4TGVcvBhz",
        "outputId": "0162af21-4376-4fbd-cded-b1d21e1499e0"
      },
      "execution_count": 9,
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "Hi there! I'm Rachael, and I'm excited to help you get set up with a new home. Could you please provide me with your full name so we can get started?\n",
            "Hi Rachael! I'm looking to buy a new home!\n",
            "Hi there! I'm excited to help you get set up with a new home. Could you please provide your full name so we can get started?\n",
            "Yeah it's dan\n",
            "Hi Dan! Could you please provide your full name so I can get started with helping you find your new home?\n",
            "oh yeah dan w\n",
            "Hi Dan! Could you please provide your full name so we can get started?\n",
            "oh you need my full legal name. Why didn't you say so! daniel warfield\n",
            "output state:\n",
            "{'first_name': 'Daniel', 'last_name': 'Warfield', 'incrementor': 4, 'conversation': [('system', '\\n        You are an AI agent tasked with doing lead qualification for a real-estate\\n        company. Your present objective is to introduce yourself as \"Rachael\", say\\n        you\\'re excited to help them get set up with a new home. After you introduce yourself,\\n        ask them for their name in full so you can get started.\\n        '), ('user', \"Hi Rachael! I'm looking to buy a new home!\"), ('system', \"\\n        The provided name appears to be incomplete. Please notify the user\\nthat the name does not appear to be complete and request that they either\\nprovide their full name or confirm that that is indeed their full name. You don't need to re-introduce yourself anymore.\\n        \"), ('user', \"Yeah it's dan\"), ('system', \"\\n        The provided name appears to be incomplete. Please notify the user\\nthat the name does not appear to be complete and request that they either\\nprovide their full name or confirm that that is indeed their full name. You don't need to re-introduce yourself anymore.\\n        \"), ('user', 'oh yeah dan w'), ('system', \"\\n        The provided name appears to be incomplete. Please notify the user\\nthat the name does not appear to be complete and request that they either\\nprovide their full name or confirm that that is indeed their full name. You don't need to re-introduce yourself anymore.\\n        \"), ('user', \"oh you need my full legal name. Why didn't you say so! daniel warfield\")]}\n"
          ]
        }
      ]
    },
    {
      "cell_type": "code",
      "source": [
        "from IPython.display import Image, display\n",
        "display(Image(app.get_graph(xray=True).draw_mermaid_png()))"
      ],
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/",
          "height": 236
        },
        "id": "jZpoCT3NVmap",
        "outputId": "d289d002-4eb4-45ab-a437-97279f8f2462"
      },
      "execution_count": 10,
      "outputs": [
        {
          "output_type": "display_data",
          "data": {
            "image/jpeg": "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\n",
            "text/plain": [
              "<IPython.core.display.Image object>"
            ]
          },
          "metadata": {}
        }
      ]
    },
    {
      "cell_type": "markdown",
      "source": [
        "# Fleshing out some more stuff\n",
        "Imagining that they need to create an account with an email, then need to provide a short code from that email. First we need to ask them if they have an email. If we do we send them to make one and tell them to come back when they're done.\n",
        "\n",
        "once they enter in some email, we \"send a code\" to that email, weather it exists or not. They then respond with that code and progress, or they have an invalid email. If their email is invalid we ask them to re-enter it."
      ],
      "metadata": {
        "id": "AN9Tf6pmVcaj"
      }
    },
    {
      "cell_type": "code",
      "source": [
        "from typing import TypedDict\n",
        "from langgraph.graph import StateGraph, START, END\n",
        "from langchain_openai import ChatOpenAI\n",
        "from langchain_core.prompts import ChatPromptTemplate\n",
        "\n",
        "# Defining state\n",
        "class GraphState(TypedDict):\n",
        "    first_name: str\n",
        "    last_name: str\n",
        "    incrementor: int\n",
        "    conversation: list[tuple[str]]\n",
        "    has_account: bool\n",
        "    email: str\n",
        "    verification_code: int\n",
        "\n",
        "workflow = StateGraph(GraphState)\n",
        "\n",
        "# ===========================\n",
        "class Name(TypedDict):\n",
        "    first_name: str\n",
        "    last_name: str\n",
        "\n",
        "name_parse_prompt = ChatPromptTemplate.from_messages(\n",
        "    [\n",
        "        (\n",
        "            \"system\",\n",
        "            \"\"\"Parse the first and last name from the users message\"\"\",\n",
        "        ),\n",
        "        (\"placeholder\", \"{messages}\"),\n",
        "    ]\n",
        ")\n",
        "\n",
        "name_parser = name_parse_prompt | ChatOpenAI(\n",
        "    model=\"gpt-4o\", temperature=0\n",
        ").with_structured_output(Name)\n",
        "\n",
        "def handle_name_request(state):\n",
        "    state['incrementor'] += 1\n",
        "\n",
        "    #if the conversation has just started, append a system prompt\n",
        "    if state['conversation'] is None:\n",
        "        system_prompt = '''\n",
        "        You are an AI agent tasked with doing lead qualification for a real-estate\n",
        "        company. Your present objective is to introduce yourself as \"Rachael\", say\n",
        "        you're excited to help them get set up with a new home. After you introduce yourself,\n",
        "        ask them for their name in full so you can get started.\n",
        "        '''\n",
        "        state['conversation'] = [('system', system_prompt)]\n",
        "\n",
        "    chat = ChatOpenAI(model=\"gpt-4o\", temperature=0)\n",
        "    ai_request_to_user = chat.invoke(state['conversation']).content\n",
        "\n",
        "    user_reponse = input(ai_request_to_user +'\\n')\n",
        "    state['conversation'].append(('user', user_reponse))\n",
        "    name = name_parser.invoke({\"messages\": state['conversation']})\n",
        "    state['first_name'] = name['first_name']\n",
        "    state['last_name'] = name['last_name']\n",
        "    return state\n",
        "\n",
        "workflow.add_node(\"name_request\", handle_name_request)\n",
        "# ===========================\n",
        "class IsAcceptable(TypedDict):\n",
        "    is_acceptable: bool\n",
        "\n",
        "name_check_prompt = ChatPromptTemplate.from_messages(\n",
        "    [\n",
        "        (\n",
        "            \"system\",\n",
        "            \"\"\"Is the name a full and complete first and last name? The name should either\n",
        "be an obvious full legal name, or the user should have confirmed that it is their full name.\n",
        "If either are true, then the name is considered acceptable.\"\"\",\n",
        "        ),\n",
        "        (\"placeholder\", \"{messages}\"),\n",
        "    ]\n",
        ")\n",
        "\n",
        "name_checker = name_check_prompt | ChatOpenAI(\n",
        "    model=\"gpt-4o\", temperature=0\n",
        ").with_structured_output(IsAcceptable)\n",
        "\n",
        "def check_full_name(state):\n",
        "    state['incrementor'] += 1\n",
        "\n",
        "    name_check = name_checker.invoke({\"messages\": state['conversation']+[(\"user\", f\"first name: \\\"{state['first_name']}\\\", last name: \\\"{state['last_name']}\\\"\")]})\n",
        "    if name_check['is_acceptable']:\n",
        "        return 'has_account_node'\n",
        "    else:\n",
        "        system_prompt = '''\n",
        "        The provided name appears to be incomplete. Please notify the user\n",
        "that the name does not appear to be complete and request that they either\n",
        "provide their full name or confirm that that is indeed their full name. You don't need to re-introduce yourself.\n",
        "        '''\n",
        "        state['conversation'].append(('system', system_prompt))\n",
        "        return 'name_request'\n",
        "\n",
        "# ===========================\n",
        "class HasAccount(TypedDict):\n",
        "    has_account: bool\n",
        "\n",
        "has_account_prompt = ChatPromptTemplate.from_messages(\n",
        "    [\n",
        "        (\n",
        "            \"system\",\n",
        "            \"\"\"Does the user have an account or not?\"\"\",\n",
        "        ),\n",
        "        (\"placeholder\", \"{messages}\"),\n",
        "    ]\n",
        ")\n",
        "\n",
        "has_account_parser = has_account_prompt | ChatOpenAI(\n",
        "    model=\"gpt-4o\", temperature=0\n",
        ").with_structured_output(HasAccount)\n",
        "\n",
        "def handle_has_account(state):\n",
        "    state['incrementor'] += 1\n",
        "\n",
        "    system_prompt = '''\n",
        "    The users name has been acquired. Now, ask if they have an account\n",
        "    already set up.\n",
        "    '''\n",
        "    state['conversation'].append(('system', system_prompt))\n",
        "\n",
        "    chat = ChatOpenAI(model=\"gpt-4o\", temperature=0)\n",
        "    ai_request_to_user = chat.invoke(state['conversation']).content\n",
        "\n",
        "    user_reponse = input(ai_request_to_user +'\\n')\n",
        "    state['conversation'].append(('user', user_reponse))\n",
        "    has_account = has_account_parser.invoke({\"messages\": state['conversation']})\n",
        "    state['has_account'] = has_account['has_account']\n",
        "    return state\n",
        "\n",
        "workflow.add_node(\"has_account_node\", handle_has_account)\n",
        "\n",
        "# ===========================\n",
        "def check_has_account(state):\n",
        "    state['incrementor'] += 1\n",
        "\n",
        "    if state['has_account']:\n",
        "        return 'get_email'\n",
        "    else:\n",
        "        system_prompt = '''\n",
        "        The user does not have an account set up. Tell them to set one up at\n",
        "        danielwarfield.dev then ask them to come back when they're done.\n",
        "        Respond directly to the previous users message as if in a continuous\n",
        "        conversation\n",
        "        '''\n",
        "        state['conversation'].append(('system', system_prompt))\n",
        "        chat = ChatOpenAI(model=\"gpt-4o\", temperature=0)\n",
        "        ai_request_to_user = chat.invoke(state['conversation']).content\n",
        "        user_reponse = input(ai_request_to_user +'\\n')\n",
        "        state['conversation'].append(('user', user_reponse))\n",
        "        return 'get_email'\n",
        "\n",
        "# ===========================\n",
        "class Email(TypedDict):\n",
        "    email: str\n",
        "\n",
        "email_parse_prompt = ChatPromptTemplate.from_messages(\n",
        "    [\n",
        "        (\n",
        "            \"system\",\n",
        "            \"\"\"What email did the user provide?\"\"\",\n",
        "        ),\n",
        "        (\"placeholder\", \"{messages}\"),\n",
        "    ]\n",
        ")\n",
        "\n",
        "email_parser = email_parse_prompt | ChatOpenAI(\n",
        "    model=\"gpt-4o\", temperature=0\n",
        ").with_structured_output(Email)\n",
        "\n",
        "def handle_request_email(state):\n",
        "    state['incrementor'] += 1\n",
        "\n",
        "    system_prompt = '''\n",
        "    The user has an email associated to their account. Ask them for it.\n",
        "    '''\n",
        "    state['conversation'].append(('system', system_prompt))\n",
        "\n",
        "    chat = ChatOpenAI(model=\"gpt-4o\", temperature=0)\n",
        "    ai_request_to_user = chat.invoke(state['conversation']).content\n",
        "\n",
        "    user_reponse = input(ai_request_to_user +'\\n')\n",
        "    state['conversation'].append(('user', user_reponse))\n",
        "    email = email_parser.invoke({\"messages\": state['conversation']})\n",
        "    state['email'] = email['email']\n",
        "    return state\n",
        "\n",
        "workflow.add_node(\"get_email\", handle_request_email)\n",
        "\n",
        "# ===========================\n",
        "class ShortCode(TypedDict):\n",
        "    short_code: int\n",
        "\n",
        "code_parse_ptompt = ChatPromptTemplate.from_messages(\n",
        "    [\n",
        "        (\n",
        "            \"system\",\n",
        "            \"\"\"Parse out the short integer code from the prompt\"\"\",\n",
        "        ),\n",
        "        (\"placeholder\", \"{messages}\"),\n",
        "    ]\n",
        ")\n",
        "\n",
        "short_code_parser = code_parse_ptompt | ChatOpenAI(\n",
        "    model=\"gpt-4o\", temperature=0\n",
        ").with_structured_output(ShortCode)\n",
        "\n",
        "def email_verify(state):\n",
        "    state['incrementor'] += 1\n",
        "\n",
        "    system_prompt = '''\n",
        "    Inform the user that a short code has been sent to their email. Ask them\n",
        "    to confirm their email with that code.\n",
        "    '''\n",
        "    state['conversation'].append(('system', system_prompt))\n",
        "\n",
        "    chat = ChatOpenAI(model=\"gpt-4o\", temperature=0)\n",
        "    ai_request_to_user = chat.invoke(state['conversation']).content\n",
        "\n",
        "    user_reponse = input(ai_request_to_user +'\\n')\n",
        "    state['conversation'].append(('user', user_reponse))\n",
        "    short_code = short_code_parser.invoke({\"messages\": state['conversation']})\n",
        "    state['verification_code'] = short_code['short_code']\n",
        "\n",
        "    return state\n",
        "\n",
        "workflow.add_node(\"email_verify\", email_verify)\n",
        "# ===========================\n",
        "def check_auth(state):\n",
        "    state['incrementor'] += 1\n",
        "\n",
        "    if state['email'] == 'hire@danielwarfield.dev' and state['verification_code'] == 1111:\n",
        "        print('Email verified. Done!')\n",
        "        return '__end__'\n",
        "    else:\n",
        "        system_prompt = '''\n",
        "        Either the user name or short code was incorrect. Ask for the email again.\n",
        "        '''\n",
        "        state['conversation'].append(('system', system_prompt))\n",
        "        chat = ChatOpenAI(model=\"gpt-4o\", temperature=0)\n",
        "        ai_request_to_user = chat.invoke(state['conversation']).content\n",
        "        user_reponse = input(ai_request_to_user +'\\n')\n",
        "        state['conversation'].append(('user', user_reponse))\n",
        "        return 'get_email'\n",
        "# ===========================\n",
        "\n",
        "# Set entry point and edges\n",
        "workflow.set_entry_point('name_request')\n",
        "workflow.add_conditional_edges(\n",
        "    'name_request',\n",
        "    check_full_name,\n",
        "    [\"name_request\", \"has_account_node\"]\n",
        ")\n",
        "workflow.add_conditional_edges(\n",
        "    'has_account_node',\n",
        "    check_has_account,\n",
        "    [\"get_email\"]\n",
        ")\n",
        "workflow.add_edge(\"get_email\", \"email_verify\")\n",
        "workflow.add_conditional_edges(\n",
        "    'email_verify',\n",
        "    check_auth,\n",
        "    [\"get_email\", '__end__']\n",
        ")\n",
        "\n",
        "# Compile and run the workflow\n",
        "app = workflow.compile()\n",
        "inputs = {\"incrementor\": 0, \"conversation\":None}  # Provide the initial state\n",
        "result = app.invoke(inputs)\n",
        "print('output state:')\n",
        "print(result)"
      ],
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "ZiNPtLPuQ3vY",
        "outputId": "a92bed69-a5df-4a66-c482-5278faba40f5"
      },
      "execution_count": 12,
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "Hi there! I'm Rachael, and I'm excited to help you get set up with a new home. Could you please provide me with your full name so we can get started?\n",
            "dan w\n",
            "Hi Dan, it looks like your name might be incomplete. Could you please provide your full name or confirm if \"Dan W\" is indeed your full name? This will help me get started on finding the perfect home for you.\n",
            "yeah beleive it or not that's actually my full name. Weird parents.\n",
            "Nice to meet you, Dan W! Do you already have an account set up with us?\n",
            "yep\n",
            "Great, Dan! Could you please provide the email address associated with your account?\n",
            "yeah it's hire@danielwarfield.dev\n",
            "Great, Dan! A short code has been sent to your email. Could you please check your inbox and provide me with that code to confirm your email?\n",
            "just got it. 1111\n",
            "Email verified. Done!\n",
            "output state:\n",
            "{'first_name': 'Dan', 'last_name': 'W', 'incrementor': 5, 'conversation': [('system', '\\n        You are an AI agent tasked with doing lead qualification for a real-estate\\n        company. Your present objective is to introduce yourself as \"Rachael\", say\\n        you\\'re excited to help them get set up with a new home. After you introduce yourself,\\n        ask them for their name in full so you can get started.\\n        '), ('user', 'dan w'), ('system', \"\\n        The provided name appears to be incomplete. Please notify the user\\nthat the name does not appear to be complete and request that they either\\nprovide their full name or confirm that that is indeed their full name. You don't need to re-introduce yourself.\\n        \"), ('user', \"yeah beleive it or not that's actually my full name. Weird parents.\"), ('system', '\\n    The users name has been acquired. Now, ask if they have an account\\n    already set up.\\n    '), ('user', 'yep'), ('system', '\\n    The user has an email associated to their account. Ask them for it.\\n    '), ('user', \"yeah it's hire@danielwarfield.dev\"), ('system', '\\n    Inform the user that a short code has been sent to their email. Ask them\\n    to confirm their email with that code.\\n    '), ('user', 'just got it. 1111')], 'has_account': True, 'email': 'hire@danielwarfield.dev', 'verification_code': 1111}\n"
          ]
        }
      ]
    },
    {
      "cell_type": "code",
      "source": [
        "from IPython.display import Image, display\n",
        "display(Image(app.get_graph(xray=True).draw_mermaid_png()))"
      ],
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/",
          "height": 488
        },
        "id": "jERwP3yfbXLl",
        "outputId": "459b7568-de4f-4ed1-e902-81f0ffc96876"
      },
      "execution_count": 14,
      "outputs": [
        {
          "output_type": "display_data",
          "data": {
            "image/jpeg": "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\n",
            "text/plain": [
              "<IPython.core.display.Image object>"
            ]
          },
          "metadata": {}
        }
      ]
    },
    {
      "cell_type": "code",
      "source": [],
      "metadata": {
        "id": "Jn2bkDxycvVi"
      },
      "execution_count": null,
      "outputs": []
    }
  ]
}